package org.gwt.mosaic.application.rebind;

import java.io.File;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;

import org.gwt.mosaic.xul.client.ElementAttributeSetter;
import org.gwt.mosaic.xul.client.ui.Element;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

public class ElementAttributeSetterGenerator extends Generator {

	private String className = null;
	private String packageName = null;

	@Override
	public String generate(TreeLogger logger, GeneratorContext context,
			String typeName) throws UnableToCompleteException {

		TypeOracle typeOracle = context.getTypeOracle();

		try {

			// get classType and save instance variables
			JClassType classType = typeOracle.getType(typeName);
			packageName = classType.getPackage().getName();
			className   = classType.getSimpleSourceName() + "Wrapper";

			// Generate class source code
			generateClass(logger, context);

		} catch (Exception e) {

			// record to logger that Map generation threw an exception
			logger.log(TreeLogger.ERROR, "ElementAttributeSetter ERROR!!!", e);

		}

		// return the fully qualifed name of the class generated
		return packageName + "." + className; 		
	}

	/**
	 * Generate source code for new class. Class implements {@link ElementAttributeSetter}
	 *
	 * @param logger Logger object
	 * @param context Generator context
	 */
	private void generateClass (TreeLogger logger, GeneratorContext context) {

		// get print writer that receives the source code
		PrintWriter printWriter = null;
		printWriter = context.tryCreate(logger, packageName, className);
		// print writer if null, source code has ALREADY been generated, return
		if (printWriter == null) return;

		// init composer, set class properties, create source writer
		ClassSourceFileComposerFactory composer = null;
		composer = new ClassSourceFileComposerFactory (packageName,
				className);

		// set implementing interfaces
		//
		composer.addImplementedInterface(ElementAttributeSetter.class.getName());
		
		// add element imports
		//
		for (String clazz : findElementClasses (true)) {
			composer.addImport (clazz);
		}

		SourceWriter sourceWriter = null;
		sourceWriter = composer.createSourceWriter (context, printWriter);
		
		sourceWriter.indent();
		sourceWriter.println("public void setAttribute (Element element, String property, String value) {");
		for (String cls : findElementClasses (false)) {
			
			sourceWriter.println("if (element.getClass().equals (" + cls + ".class)) {");
			sourceWriter.indent();
			sourceWriter.println (cls + " o = (" + cls + ") element;");
			
			try {
				
				// load the class
				//
				Class<?> clazz = Class.forName ("org.gwt.mosaic.xul.client.ui." + cls);

				for (Field field : findFieldsInHierarchy (clazz, Element.class)) {
					
					//if ((field.getModifiers() & Modifier.STATIC) != 0)
					//	continue;
					
					String fieldName = field.getName().toLowerCase();
					String upcaseFieldName = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring (1);
					
					String setterName = "set" + upcaseFieldName;
					Method setterMethod = methodExists(clazz, Element.class, setterName);
					if (setterMethod != null) { 

						sourceWriter.println("if (property.equalsIgnoreCase (\"" + fieldName + "\")) {");
						sourceWriter.indent();
						
						Class<?> parameterClass = setterMethod.getParameterTypes() [0];
						
						if (parameterClass.equals(String.class))
							sourceWriter.println("o.set" + upcaseFieldName + " (value);");
						else if (parameterClass.equals(Integer.class) || parameterClass.getName().equals("int"))
							sourceWriter.println("o.set" + upcaseFieldName + " (Integer.parseInt (value));");
						else if (parameterClass.equals(Boolean.class) || parameterClass.getName().equals("boolean"))
							sourceWriter.println("o.set" + upcaseFieldName + " (Boolean.parseBoolean (value));");
						else if (parameterClass.equals(Float.class) || parameterClass.getName().equals("float"))
							sourceWriter.println("o.set" + upcaseFieldName + " (Float.parseFloat (value));");
						else if (parameterClass.equals(Double.class) || parameterClass.getName().equals("double"))
							sourceWriter.println("o.set" + upcaseFieldName + " (Double.parseDouble (value));");
						//else if (parameterClass.equals(Character.class) || parameterClass.getName().equals("char")) TODO
						//	sourceWriter.println("o.set" + upcaseFieldName + " (Character.parse");
						else {
							
							// did i forget to map something?
							//
							
							System.out.println ("unknown parameter type: " + setterMethod.getParameterTypes() [0].getSimpleName());
						}
						
						sourceWriter.println("return;");
						sourceWriter.outdent();
						sourceWriter.println("}\n");

					} else {
						
						// no method was found - this field is not intended to be set using setters (
						// maybe it is used internally or has no setter)
						//
						
						//System.out.println ("For " + clazz + ", method " + setterName + " does not exist");
						//
					}

				}
				
			} catch (Exception e) {
				e.printStackTrace();
				// continue with execution
			}
			
			sourceWriter.outdent(); 
			sourceWriter.println("}");
		}
		
		sourceWriter.outdent();
		
		sourceWriter.println("}");
		sourceWriter.outdent();
		sourceWriter.println("}");

		// commit generated class
		context.commit(logger, printWriter);
	}

	
	public String[] findElementClasses (boolean appendPackageName) {
		
		String pkgName = "org.gwt.mosaic.xul.client.ui";

		ArrayList<String> classes = new ArrayList<String>();
		String name = new String (pkgName);

		if (!name.startsWith("/")) {
			name = "/" + name;
		}        
		name = name.replace('.','/');

		URL url = Element.class.getResource(name);
		File directory = new File(url.getFile());
		
		if (directory.exists()) {
			// Get the list of the files contained in the package
			String [] files = directory.list();
			for (int i=0;i<files.length;i++) {

				if (files[i].endsWith(".class")) {
					String classname = files[i].substring(0,files[i].length()-6);
					String finalClassName = appendPackageName ? (pkgName + "." + classname) : classname;
					
					if (!classname.contains("$") && !classes.contains (finalClassName)) {
						classes.add (finalClassName);
					}
				} else if (files [i].endsWith(".java")) {
					String classname = files[i].substring(0, files[i].length() - 5);
					String finalClassName = appendPackageName ? (pkgName + "." + classname) : classname;
					
					if (!classname.contains("$") && !classes.contains(finalClassName)) {
						classes.add (finalClassName);
					}				
				}
			}
		}
		
		return classes.toArray(new String [classes.size()]);
	}
	
	public Field[] findFieldsInHierarchy (Class<?> start, Class<?> end) {
		ArrayList<Field> fields = new ArrayList<Field>();
		
		try {
			
			Class<?> current = start;
			while (!current.equals(end)) {
				for (Field f : current.getDeclaredFields()) {
					
					// check this field 
					//
					//if ((f.getModifiers() & Modifier.FINAL) != 0)
					//	continue;
					
					fields.add (f);
				}
				
				current = current.getSuperclass();
			}
			
			// check if end class is null
			//
			if (current != null) {

				for (Field f : current.getDeclaredFields()) {
					
					// check this field 
					//
					//if ((f.getModifiers() & Modifier.FINAL) != 0)
					//	continue;
					
					fields.add (f);
				}				
				
			}
			
		} catch (Exception e) {
			// not interested in this
		}
		
		return fields.toArray(new Field [fields.size()]);
	}
	
	
	private Method[] findMethodsInHierarchy (Class<?> start, Class<?> end) {
		ArrayList<Method> methods = new ArrayList<Method>();
		
		try {
			
			Class<?> current = start;
			while (!current.equals(end)) {
				
				for (Method m : current.getMethods()) {
					
					methods.add(m);
				}
				
				current = current.getSuperclass();
			}
			
			// check if end class is null
			//
			if (current != null) {

				for (Method m : current.getMethods()) {
					
					methods.add(m);
				}				
				
			}
			
		} catch (Exception e) {
			// not interested in this
		}
		
		return methods.toArray(new Method [methods.size()]);		
	}
	
	
	/**
	 * Check to see if the method exists somewhere in class hierarchy
	 * @param target
	 * 		The start class. Methods should be retrieved for superclasses too
	 * @param end
	 * 		The end class
	 * @param methodName
	 * 		Name of the method
	 * @return
	 */
	private Method methodExists (Class<?> target, Class<?> end, String methodName) {
		
		try {
			for (Method method : this.findMethodsInHierarchy(target, end)) {
		
				if (method.getName().equals (methodName))
					return method;
			}
			
			return null;
		} catch (Exception e) {
			return null;
		}
	}
}
